package zmcc.bsp.fabric.config;

import zmcc.bsp.fabric.exception.FabricException;
import zmcc.bsp.fabric.gateway.FabricContract;
import zmcc.bsp.fabric.model.request.FabricQueryRequest;
import zmcc.bsp.fabric.model.request.FabricRequest;
import zmcc.bsp.fabric.model.response.FabricQueryResponse;
import zmcc.bsp.fabric.model.response.FabricResponse;
import org.hyperledger.fabric.gateway.Gateway;
import org.hyperledger.fabric.gateway.Network;
import org.hyperledger.fabric.gateway.Wallet;
import org.hyperledger.fabric.gateway.Wallet.Identity;
import org.hyperledger.fabric.gateway.impl.ContractImpl;
import org.hyperledger.fabric.gateway.impl.InMemoryWallet;
import org.hyperledger.fabric.gateway.impl.WalletIdentity;
import org.hyperledger.fabric.sdk.Enrollment;
import org.hyperledger.fabric.sdk.NetworkConfig;
import org.hyperledger.fabric.sdk.NetworkConfig.OrgInfo;
import org.hyperledger.fabric.sdk.NetworkConfig.UserInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class FabricContext {
	private Logger logger = LoggerFactory.getLogger(FabricContext.class);

	protected Gateway.Builder builder;
	protected Network network;
	protected Map<String,FabricContract> fabricContracts;

	protected FabricProperties properties;

	public FabricContext() {
	}

	public FabricContext(FabricProperties properties) {
		this.initialize(properties);
	}

	public FabricContext initialize(FabricProperties properties) throws FabricException {
		this.properties = properties;
		logger.debug("Initialize Fabric Context");

		logger.debug("Initialize Fabric Context with properties: " + properties);

		String channel = properties.getChannel();
		if (channel == null || channel.equals("")) {
			throw new FabricException(
					"Initialize fabric gateway failed with invalid 'channel' name. Please make sure the channel is configured corrected by 'spring.fabric.channel'.");
		}

		List<FabricChaincodeProperties> chaincodes = properties.getChaincodeList();
		if (chaincodes == null || chaincodes.size()==0) {
			throw new FabricException(
					"Initialize fabric gateway failed with invalid 'chaincode' name. Please make sure the chaincode is configured corrected by 'spring.fabric.chaincode'.");
		}

		InputStream configFile = properties.getNetworkContents();
		if (configFile == null) {
			throw new FabricException(
					"Network config file can not be loaded. Please make sure 'spring.fabric.network.path' is configured.");
		}

		NetworkConfig config = null;
		try {
			config = NetworkConfig.fromYamlStream(configFile);
		} catch (Exception e) {
			throw new FabricException("Network config can not be loaded.", e);
		}
		if (config == null) {
			throw new FabricException(
					"Network config can not be loaded. Please make sure 'spring.fabric.network.path' is correct.");
		}

		FabricGatewayProperties gatewayProps = properties.getGateway();

		FabricWalletProperties walletProps = gatewayProps.getWallet();

		String identityName = walletProps.getIdentity();
		if (identityName == null || identityName.equals("")) {
			identityName = "admin";
			logger.debug("Initialize Fabric Context: missing identity for wallet, and using default value: admin");
		}

		Wallet wallet = createWallet(walletProps);

		if (wallet instanceof InMemoryWallet) {
			OrgInfo client = config.getClientOrganization();
			if (client != null) {
				UserInfo peerAdmin = client.getPeerAdmin();
				if (peerAdmin != null) {
					Enrollment enrollment = peerAdmin.getEnrollment();
					if (enrollment != null) {
						Identity admin = new WalletIdentity(peerAdmin.getMspId(), enrollment.getCert(),
								enrollment.getKey());
						try {
							wallet.put(identityName, admin);
						} catch (IOException e) {
							throw new FabricException("Initialize Wallet failed", e);
						}
						logger.debug("Initialize Wallet " + identityName);
					}
				}
			}
		}

		try {
			if (!wallet.exists(identityName)) {
				throw new FabricException("Initialize Wallet failed, there's no identity = '" + identityName
						+ "' exists in wallet directory: " + walletProps.getFile());
			}
		} catch (IOException e1) {
			throw new FabricException("Initialize Wallet failed, there's no identity = '" + identityName
					+ "' exists in wallet directory: " + walletProps.getFile());
		}

		logger.debug("Initialize Gateway... ");
		configFile = properties.getNetworkContents();
		try {
			builder = Gateway.createBuilder().identity(wallet, identityName).networkConfig(configFile);
			builder.commitTimeout(gatewayProps.getCommitTimeout(), TimeUnit.SECONDS);
			builder.discovery(gatewayProps.isDiscovery());
		} catch (IOException e) {
			throw new FabricException("Initialize Gateway failed", e);
		}
		Gateway gateway = builder.connect();

		logger.debug("Initialize Network... ");

		network = gateway.getNetwork(channel);
		initContract(properties);
		return this;
	}

	private Wallet createWallet(FabricWalletProperties config) {
		if (config == null || config.isMemory() || config.getFile() == null || config.getFile().equals("")) {
			return Wallet.createInMemoryWallet();
		} else {
			try {
				return Wallet.createFileSystemWallet(new File(config.getFile()).toPath());
			} catch (IOException e) {
				return Wallet.createInMemoryWallet();
			}
		}
	}

	private Map<String,FabricContract> initContract(FabricProperties properties) {
		if (fabricContracts==null){
			fabricContracts = new ConcurrentHashMap<>();
		}
		List<FabricChaincodeProperties> chaincodes = properties.getChaincodeList();
		for (FabricChaincodeProperties chaincode :chaincodes){
			if (network == null) {
				Gateway gateway = builder.connect();
				network = gateway.getNetwork(properties.getChannel());
			}
			fabricContracts.put(chaincode.getIdentify(),getContract(chaincode.getIdentify()));
		}

		return fabricContracts;
	}
	private FabricContract getContract(String identify){
		FabricContract fabricContract = new FabricContract((ContractImpl) network.getContract(identify));
		FabricGatewayProperties gatewayProps = properties.getGateway();
		if (gatewayProps != null) {
			fabricContract.setOrdererTimeout(gatewayProps.getOrdererTimeout());
			fabricContract.setProposalTimeout(gatewayProps.getProposalTimeout());
		}
		return fabricContract;
	}

	public <T> FabricQueryResponse<T> query(String chaincodeId,FabricQueryRequest<T> queryRequest) {
		if (queryRequest == null) {
			return FabricQueryResponse.failure("Query request can not be null.");
		}
		FabricContract fabricContract = fabricContracts.get(chaincodeId);
		if (fabricContract==null){
			fabricContract = getContract(chaincodeId);
			fabricContracts.put(chaincodeId,fabricContract);
		}
		try {
			queryRequest.checkValidate();

			byte[] payload = fabricContract.evaluateTransaction(queryRequest.function, queryRequest.arguments);

			return FabricQueryResponse.create(payload, queryRequest.type);
		} catch (Exception e) {
			return FabricQueryResponse.failure(e.getMessage());
		}
	}

	public <T> FabricQueryResponse<List<T>> queryArray(String identity,Class<T> type, String function, String... arguments) {
		try {
			FabricQueryRequest<T> request = new FabricQueryRequest<T>(type, function, arguments);
			return queryMany(identity,request);
		} catch (Exception e) {
			return FabricQueryResponse.failure(e.getMessage());
		}

	}

	public <T> FabricQueryResponse<List<T>> queryMany(String chaincodeId,FabricQueryRequest<T> queryRequest) {
		if (queryRequest == null) {
			return FabricQueryResponse.failure("Query request can not be null.");
		}
		FabricContract fabricContract = fabricContracts.get(chaincodeId);
		if (fabricContract==null){
			fabricContract = getContract(chaincodeId);
			fabricContracts.put(chaincodeId,fabricContract);
		}
		try {
			queryRequest.checkValidate();

			byte[] payloads = fabricContract.evaluateTransaction(queryRequest.function, queryRequest.arguments);
			return FabricQueryResponse.many(payloads, queryRequest.type);

		} catch (Exception e) {
			return FabricQueryResponse.failure(e.getMessage());
		}
	}

	public FabricResponse execute(String chaincodeId,FabricRequest request) {
		if (request == null) {
			return FabricResponse.fail("Request can not be null");
		}
		FabricContract fabricContract = fabricContracts.get(chaincodeId);
		if (fabricContract==null){
			fabricContract = getContract(chaincodeId);
			fabricContracts.put(chaincodeId,fabricContract);
		}
		try {
			String result = fabricContract.executeTransaction(request.function, request.arguments);
			return FabricResponse.ok().setTransactionId(result);
		} catch (Exception e) {
			e.printStackTrace();
			return FabricResponse.fail(e.getMessage());
		}
	}

	public Gateway.Builder getBuilder() {
		return builder;
	}

	public Network getNetwork() {
		return network;
	}

	public Map<String, FabricContract> getFabricContract() {
		return fabricContracts;
	}
}
